package com.nulldev.util.os;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

import com.nulldev.util.io.FileUtil;
import com.nulldev.util.java.ProcessUtil;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.math.MathUtil;
import com.nulldev.util.os.OSUtil.OSType;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.string.StringsUtil;

public class CPUUtil {
	
	public static enum Arch {

		x86("x86", "i386", "i486", "i586", "i686"),
		x86_64("x86_64", "amd64", "k8"),
		ARM("ARM"),
		UNKNOWN("Unknown");
		
		private String[] search;
		
		private Arch(String... search) {
			this.search = search;
		}
		
		public String getName() {
			return search[0];
		}
		
		public String[] getSearch() {
			return search;
		}
		
		/**
		 * Gets arch, either 64-bit or x86
		 * @param arch
		 * @return
		 */
		public static Arch getArch(String s) {
			for (Arch arch : Arch.values())  {
				for (String search : arch.getSearch()) {
					if (s.equalsIgnoreCase(search)) {
						return arch;
					}
				}
			}
			
			return Arch.UNKNOWN;
		}
		
		/**
		 * Gets this machines arch
		 * @return
		 */
		public static Arch getArch() {
			return getArch(getArchString());
		}
		
		/**
		 * Same as System.getProperty("os.arch");
		 * @return
		 */
		public static String getArchString() {
			return System.getProperty("os.arch");
		}

	}

	public static enum Vendor{
		INTEL("Intel(tm)", "GenuineIntel"),
		AMD("AMD(tm)", "AuthenticAMD"),
		AMD_ALT("AMD(tm)", "AMDisbetter!"),
		CYRIX("Cyrix", "CyrixInstead"),
		VIA("VIA Technologies", "VIA VIA VIA "),
		VIA_ALT("VIA Technologies", "CentaurHauls"),
		TRANSMETA("Transmeta", "TransmetaCPU"),
		TRANSMETA_ALT("Transmeta", "GenuineTMx86"),
		NSC("NSC", "Geode by NSC"),
		RISE("Rise", "RiseRiseRise"),
		SIS("SiS", "SiS SiS SiS "),
		UMC("UMC", "UMC UMC UMC "),
		NEXGEN("NexGen", "NexGenDriven"),
		VIRTUAL_MACHINE("Virtual CPU", "system.multiple()"),
		VORTEX_86("Vortex", "Vortex86 SoC"),
		UNKNOWN("N/A", "N/A")

		;
		private String displayName, id;
		private Vendor(final String displayName, final String id) {
			this.displayName = displayName;
			this.id = id;
		}

		@Override
		public String toString() {
			return this.displayName;
		}

		public String displayName() {
			return this.displayName;
		}

		public String id() {
			return this.id;
		}

		public String vendor_ascii() {
			return "";
		}
	}
	
	public static enum VM_Vendor {
		NOT_VM("Not a virtual machine!", "NVM"),
		
		BHYVE("BHYVE VM", "bhyve bhyve "),
		KVM("KVM", "KVMKVMKVM"),
		HYPERV("HyperV", "Microsoft Hv"),
		PARAELLS("Paraells", " lrpepyh vr"),
		VMWARE("VMWare", "VMwareVMware"),
		XEN("XenVM", "XenVMMXenVMM"),
		
		;
		private String displayName, id;
		private VM_Vendor(final String displayName, final String id) {
			this.displayName = displayName;
			this.id = id;
		}

		@Override
		public String toString() {
			return this.displayName;
		}

		public String displayName() {
			return this.displayName;
		}

		public String id() {
			return this.id;
		}

		public String vendor_ascii() {
			return "";
		}

		public static VM_Vendor get(final String env) {
			for (final VM_Vendor v : values()) {
				if (v.id().equals(env)) {
					return v;
				}
			}
			return NOT_VM;
		}
	}

	public static enum Brand{
		UNKNOWN("Unknown", -1),

		/*
		 * Intel
		 */
		Core_Series("Intel Core Series", 6),
		Core_Series_2nd_Gen_Mobile("Intel Core Series: 2nd Generation Mobile", 6, 42),
		
		/*
		 * AMD
		 */
		Hammer("AMD Althon 64", 15), K10("Phenom/Opteron", 16), Bobcat("AMD FX/A Series (Gen. I)", 20), Piledriver_Bulldozer("AMD FX/A Series (Gen. II + III)", 21),
		Jaguar("Jaguar" , 22), Zen("Ryzen", 22), A_Series("AMD A Series [Gen 2]", -21), A_Series_G1("AMD A Series [Gen 1]", -20)

		;
		private String displayName;
		private int family;
		
		private Brand(final String displayName, final int family) {
			this(displayName, family, -1);
		}
		
		private Brand(final String displayName, final int family, final int model) {
			this.displayName = displayName;
			this.family = family;
		}

		public int getFamily() {
			return this.family;
		}

		@Override
		public String toString() {
			return this.displayName;
		}

		public String displayName() {
			return this.displayName;
		}

		public static Brand fromID(final int id) {
			for (final Brand br : values()) {
				if (br.getFamily() == id) {
					return br;
				}
			}
			System.out.println("Brand.fromID -> " + id);
			return Brand.UNKNOWN;
		}
	}

	public static class CPU {

		private Vendor vendor;
		private Brand brand;
		private String data;
		private String[] array;

		public CPU(final Vendor vendor, final String data) {
			this.data = data;
			this.array = data.split(" ");
			this.vendor = vendor;
			this.brand = Brand.UNKNOWN;
		}

		public String getCPUData() {
			return this.data;
		}

		public Vendor getVendor() {
			return this.vendor;
		}
		
		public VM_Vendor getVMVendor() {
			if (!isVM()) {
				return VM_Vendor.NOT_VM;
			}
			return VM_Vendor.get(System.getenv("PROCESSOR_IDENTIFIER"));
		}

		public Brand getBrand() {
			if (this.vendor == Vendor.AMD && OSUtil.isWindows()) {
				if (StringsUtil.isEqual(this.array, 1, "Family")) {
					final int val = MathUtil.safeParseList(this.array, 2, -1);
					if (val != -1) {
						if (data.contains("AMD A") && data.contains("APU")) {
							return Brand.fromID(-val);
						}else {
							return Brand.fromID(val);
						}
					}
				}
			}else if (this.vendor == Vendor.AMD && OSUtil.isLinux()) {
				for (final String entry : this.array) {
					final String[] val = entry.split(":");
					if (val[0].equals("cpu family")) {
						final int x = MathUtil.safeParseI(val[1].substring(1), -1);
						if (x != -1) {
							if (data.contains("AMD A") && data.contains("APU")) {
								return Brand.fromID(-x);
							}else {
								return Brand.fromID(x);
							}
						}
					}
				}
			}else if (this.vendor == Vendor.INTEL) {
				for (final String entry : this.array) {
					final String[] val = entry.split(":");
					if (val[0].equals("cpu family")) {
						final int x = MathUtil.safeParseI(val[1].substring(1), -1);
						return Brand.fromID(x);
					}
				}
			}
			return this.brand;
		}

		public boolean isVM() {
			return this.vendor == Vendor.VIRTUAL_MACHINE;
		}

		public String arch() {
			return System.getenv("PROCESSOR_ARCHITECTURE");
		}

		public boolean isX86() {
			if (this.arch() != null) {
				return this.arch().equals("x86");
			}else {
				return true;
			}
		}
		
		/**
		 * ONLY USED BY parse()
		 */
		protected void override_data(final String[] data) {
			if (!Thread.currentThread().getStackTrace()[2].getMethodName().equals("parse")) {
				LoggerFactory.getLogger().warn("Unsafe function called!");
			}
			this.array = data;
		}

		@Override
		public String toString() {
			final StringBuilder out = new StringBuilder();
			out.append("CPU[cpu=" + this.data + ",vendor=" + this.vendor.displayName() + ",brand=" + this.getBrand().displayName() + ",amd64=" + this.isAMD64() + ",virtual_machine=" + this.isVM() + "]");
//			out.append("nullUtil.CPU{");
//			out.append(this.data);
//			out.append(" | by " + this.vendor);
//			out.append(", Reported Brand: " + this.getBrand() + ". This is " + (this.isAMD64() ? "a" : "not a") + " AMD64 CPU.");
//			out.append(" This is " + (this.vendor == Vendor.VIRTUAL_MACHINE ? "a" : "not a") + " virtual machine.");
//			out.append("}");
			return out.toString();
		}

		private boolean __isAMD64() {
			if (this.vendor == Vendor.AMD || this.vendor == Vendor.AMD_ALT) {
				return StringsUtil.isEqual(this.array, 0, "AMD64") || ArrayUtils.contains(getArch().getSearch(), "amd64") || System.getProperty("sun.cpu.isalist").contains("amd64");
			}else if (this.vendor == Vendor.INTEL) {
				return false;
			}
			return false;
		}

		public boolean isAMD64() {
			return this.is64Bit;
		}
		
		public boolean isCVE_2018Safe() {
			if (this.vendor == Vendor.AMD || this.vendor == Vendor.VIA || this.vendor == Vendor.VIRTUAL_MACHINE) {
				return true;
			}
			return false;
		}

		public void println() {
			synchronized (System.out) {
				synchronized (System.err) {
					System.out.println(this.toString());
				}
			}
		}

		private boolean is64Bit = __isAMD64();
		public void override64(final boolean _64bit) {
			this.is64Bit = _64bit;
		}
		
		public Arch getArch() {
			return Arch.getArch();
		}
	}

	public static CPU parse() {
		if (OSUtil.getOperatingSystemType() == OSType.Windows) {
			final String CPU_INFO = System.getenv("PROCESSOR_IDENTIFIER");
			if (CPU_INFO == null) {
				return new CPU(Vendor.UNKNOWN, "Unknown Brand, Unknown CPU");
			}
			if (CPU_INFO.contains("AuthenticAMD")) {
				return new CPU(Vendor.AMD, CPU_INFO);
			}else if (CPU_INFO.contains("GenuineIntel")) {
				return new CPU(Vendor.INTEL, CPU_INFO);
			}else if (CPU_INFO.contains("VIA VIA VIA ")) {
				return new CPU(Vendor.VIA, CPU_INFO);
			}else if (CPU_INFO.contains("bhyve bhyve ") || CPU_INFO.contains("KVMKVMKVM") || CPU_INFO.contains("Microsoft Hv") || CPU_INFO.contains(" lrpepyh vr") || CPU_INFO.contains("VMwareVMware") || CPU_INFO.contains("XenVMMXenVMM")) {
				return new CPU(Vendor.VIRTUAL_MACHINE, CPU_INFO);
			}
			return new CPU(Vendor.UNKNOWN, "Unknown Brand, Unknown CPU");
		}else if (OSUtil.getOperatingSystemType() == OSType.Linux) {
			try {
				final String filtered = StringsUtil.test_linux_parse(ProcessUtil.execCmd("cat /proc/cpuinfo"));
				//UnicodeUtils.printA(filtered);
				final String[] data = StringsUtil.byLineSepeartorAI(filtered);
				final String[] raw = StringsUtil.byLineSepeartor(filtered);
				Vendor vendor = Vendor.UNKNOWN;
				String cpu_name = "Unknown Brand, Unknown CPU";
				for (final String entry : data) {
					final String[] val = entry.split(":");
					if (val[0].equals("vendor_id")) {
						final String CPU_INFO = val[1].substring(1);
						if (CPU_INFO.contains("AuthenticAMD")) {
							vendor = Vendor.AMD;
						}else if (CPU_INFO.contains("GenuineIntel")) {
							vendor = Vendor.INTEL;
						}else if (CPU_INFO.contains("VIA VIA VIA ")) {
							vendor = Vendor.VIA;
						}else if (CPU_INFO.contains("bhyve bhyve ") || CPU_INFO.contains("KVMKVMKVM") || CPU_INFO.contains("Microsoft Hv") || CPU_INFO.contains(" lrpepyh vr") || CPU_INFO.contains("VMwareVMware") || CPU_INFO.contains("XenVMMXenVMM")) {
							vendor = Vendor.VIRTUAL_MACHINE;
						}
					}else if (val[0].equals("model name")) {
						cpu_name = val[1].substring(1).replaceAll("_NL_", "");
					}
				}
				final CPU cpu = new CPU(vendor, cpu_name);
				cpu.override_data(raw);
				return cpu;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return new CPU(Vendor.UNKNOWN, "Unknown Brand, Unknown CPU");
	}

	public static String getArch() {
		return System.getProperty("os.arch");
	}
	
	public static String[] getBackdoors() {
		final ArrayList<String> output = new ArrayList<String>();
		final CPU cpu = parse();
		if (OSUtil.isLinux()) {
			final File imeIF = FileUtil.safeCreate("/dev/mei0");
			if (imeIF.exists()) {
				output.add("Intel.IME");
			}
		}
		if (cpu.getVendor().equals(Vendor.AMD) || cpu.getVendor().equals(Vendor.AMD_ALT)) {
			if (cpu.getBrand().equals(Brand.Zen) || cpu.getBrand().equals(Brand.A_Series) || cpu.getBrand().equals(Brand.Jaguar) || cpu.getBrand().equals(Brand.A_Series_G1)) {
				output.add("AMD.PSP");
			}
		}
		return ArrayUtils.arrayListToArray(output);
	}

	public static String[] getBugs() {
		if (getArch().equals("amd64") || getArch().equals("x86")) {
			if (OSUtil.isLinux())  {
				String filtered = "";
				try {
					filtered = StringsUtil.test_linux_parse(ProcessUtil.execCmd("cat /proc/cpuinfo"));
				} catch (IOException e) {
				}
				final String[] data = StringsUtil.byLineSepeartorAI(filtered);
				final String[] raw = StringsUtil.byLineSepeartor(filtered);
				final ArrayList<String> bugs = new ArrayList<String>();
				for (final String s : raw) {
					final String[] sp = s.split(": ");
					if (StringsUtil.isEqualCM(sp, 0, "bugs")) {
						final String[] bs = sp[1].split(" ");
						for (final String b : bs) {
							bugs.add(b);
						}
						break;
					}
				}
				return ArrayUtils.listToArray(bugs);
			}
		}
		return new String[0];
	}
}
